//@author A0100965J
package gui;

import hirondelle.date4j.DateTime;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Toolkit;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.List;

import javax.swing.BorderFactory;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.JTextPane;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultCaret;
import javax.swing.text.Document;
import javax.swing.text.Style;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyleContext;

import common.AttributeType;
import common.DateHelper;
import common.Task;
import common.TaskStatus;


import logic.Parser;
import memory.History;
import memory.Database;

/**
 * Console UI for user to interact with the program
 */
public class MainConsole extends JFrame implements IUserInterface {
	private static MainConsole _console = null;
	private JTextPane _display;
	private JScrollPane _scrollableDisplay;
	private Parser _parser;
	private Database _storage;
	private History _history;
	protected boolean _isAtHelpPage;

	private static final long serialVersionUID = 93847584938475L;
	// Message to be displayed
	private static final String WELCOME_MESSAGE = " Welcome to JimmyMemo!\n\n";
	private static final String HELP_MESSAGE = " For help, press F10.\n\n";
	private static String TODAY_MESSAGE = " Today is %1s\n\n" +
			" The following task(s) is(are) due today: \n";
	private static final String NO_RESULT_MESSAGE = " There is no result.\n\n";
	private static final String TODAY_DATE = "%04d-%02d-%02d";
	private static final String EMPTY_LINE = "\n";
	private static final String EMPTY_STRING = "";
	private static final char SPACE = ' ';
	
	// Length of fields in a display table
	private static int LENGTH_NAME_FIELD = 20;
	private static final int LENGTH_NAME_MINIMUM = 20;
	private static int LENGTH_DESCRIPTION_FIELD = 20;
	private static final int LENGTH_DESCRIPTION_MINIMUM = 20;
	private static final double INCREASE_MINIMUM = 1.1;
	private static final double INCREASE_MULTIPLE = 1.5;
	
	// Display format of a list of tasks
	private static String TABLE_FORMAT_NORMAL = "%1$-3s %2$-" + LENGTH_NAME_FIELD + "s | %3$-" + LENGTH_DESCRIPTION_FIELD + "s | %4$-25s | %5$-10s\n";
	private static final String TABLE_FORMAT_FREE = "%1$-3s %2$-25s | %3$-25s \n";
	// Dimension of components;
	private static final Dimension DIMENSION_TEXTFIELD = new Dimension(600, 30);
	private static final Dimension DIMENSION_TEXTAREA = new Dimension(600, 420);
	private static final Dimension DIMENSION_MINIMUM = new Dimension(700, 500);
	private static final int INCREASE_STEP = 10;
	// Font
	private static final Font FONT_COMMAND_LINE = new Font("monospaced", Font.PLAIN, 12);
	
	// A text file containing the help message
	private static final String FILENAME = "help.txt";

	public MainConsole() {
		_isAtHelpPage = false;
		
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		
		this.setTitle("JimmyMemo");
		
		setDisplay();
		JTextField cmdLine = setCmdLine();

		setPanel(cmdLine);
		
		pack();
		this.start();

		setCmdLineBehavior(cmdLine);
		
		setVisible(true);
	}

	/**
	 * Add JScrollPane and JTextField to a JPanel
	 * @param cmdLine	The command line
	 */
	private JPanel setPanel(JTextField cmdLine) {
		JPanel panel = new JPanel(new BorderLayout());
		panel.add(_scrollableDisplay, BorderLayout.NORTH);
		panel.add(cmdLine, BorderLayout.SOUTH);
		panel.setBackground(Color.WHITE);
		panel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
		getContentPane().add(panel);
		setMinimumSize(DIMENSION_MINIMUM);
		
		return panel;
	}

	/**
	 * Set up the command Line
	 * @param cmdLine	The command line
	 */
	private JTextField setCmdLine() {
		JTextField cmdLine = new JTextField();
		cmdLine.setFont(FONT_COMMAND_LINE);
		cmdLine.setPreferredSize(DIMENSION_TEXTFIELD);
		cmdLine.setScrollOffset(10);
		cmdLine.setBorder(BorderFactory.createLineBorder(Color.GRAY));
		
		return cmdLine;
	}

	/**
	 * Set up the display area
	 */
	private void setDisplay() {
		// display area
		_display = new JTextPane();
		_display.setEditable(false);
		initializeStyles();
		// auto scroll to the bottom
		DefaultCaret caret = (DefaultCaret) _display.getCaret();
		caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE);
		_scrollableDisplay = new JScrollPane(_display);
		_scrollableDisplay.setPreferredSize(DIMENSION_TEXTAREA);
		_scrollableDisplay.setBorder(BorderFactory.createEmptyBorder());
	}
	
	/**
	 * Set auto-focus on command line and add key listener to it
	 * @param cmdLine
	 */
	private void setCmdLineBehavior(JTextField cmdLine) {
		cmdLine.setFocusTraversalKeysEnabled(false);
		cmdLine.requestFocusInWindow();
		// Declare key listener
		KeyMonitor cmd = new KeyMonitor(_scrollableDisplay, cmdLine, _parser);
		cmdLine.addKeyListener(cmd);
	}
	
	/**
	 * @return The instance of the singleton MainConsole
	 */
	public static MainConsole getInstance() {
		if (_console == null) {
			_console = new MainConsole();
		}

		return _console;
	}

	/**
	 * Initialise the fonts that are used for _displaying
	 */
	private void initializeStyles() {
		setStyle("normal", "monospaced", 12, false, false);
		setStyle("highlight", "monospaced", 12, false, true);
		setStyle("bold", "arial", 14, true, false);
		setStyle("subheading", "arial", 12, true, false);
	}
	
	/** 
	 * Create a style with the specified requirements
	 * @param name	Name of the style
	 * @param font	Font used
	 * @param size	Font size
	 * @param isBold	Whether the words will be bold
	 * @param isHighlight	Whether the words will be highlighted in red
	 */
	private void setStyle(String name, String font, int size, boolean isBold, boolean isHighlight) {
		StyleContext context = StyleContext.getDefaultStyleContext();
		Style defaultStyle = context.getStyle(StyleContext.DEFAULT_STYLE);
		
		Style newStyle = _display.addStyle(name, defaultStyle);
		StyleConstants.setFontFamily(newStyle, font);
		StyleConstants.setFontSize(newStyle, size);
		
		if (isBold) {
			StyleConstants.setBold(newStyle, true);
		}
		if (isHighlight) {
			StyleConstants.setForeground(newStyle, Color.RED);
		}
	}
	
	/**
	 * Insert a message to the text pane with the specified style
	 * @param style	A stylized font
	 * @param msg	The message to be displayed
	 */
	private void insert(Style style, String msg) {
		Document doc = _display.getDocument();
		
		try {
			doc.insertString(doc.getLength(), msg, style);
		} catch (BadLocationException e) {
		}
	}

	/**
	 * Print the message with format arguments. <br/>
	 * @param msg	The message to be printed
	 * @param args	An array of arguments to be formatted into the message
	 */
	public void writeMessage(String msg, String... args) {
		msg = String.format(msg, (Object[]) args);
		insert(_display.getStyle("subheading"), msg);
	}

	//@author A0100965J
	/**
	 * Write the Task information in a proper way to the screen <br/>
	 * Task |Description |Time |Status 
	 * 
	 * @param tasks	The list of tasks to be formatted 
	 */
	public void writeTasks(List<Task> tasks) {
		if (tasks.size() == 0) {
			insert(_display.getStyle("normal"), NO_RESULT_MESSAGE);
			return;
		}
		
		// Adjust the size of the table according to the current window size
		adjustTableSize();
		
		int index = 1;

		String heading = String.format(TABLE_FORMAT_NORMAL, EMPTY_STRING, "Task", "Description", "Time", "Status");
		insert(_display.getStyle("normal"), heading);
		
		for (Task task : tasks) {
			index = outputTaskDetails(index, task);
		}
	}

	/**
	 * Write the details of a single task to text pane, formatted in a table form
	 * @param index	The index of the task in the table
	 * @param task	The task
	 * @return	The next index
	 */
	private int outputTaskDetails(int index, Task task) {
		String taskDetail = formatTaskDetails(index, task);
		index++;
		
		if (task.getStatus() == TaskStatus.overdue) {
			insert(_display.getStyle("highlight"), taskDetail);
		} else {
			insert(_display.getStyle("normal"), taskDetail);
		}
		insert(_display.getStyle("normal"), EMPTY_LINE);
		return index;
	}
	
	/**
	 * Format the task details into a table format
	 * @param index	The index of the task in the list
	 * @param task	The task to be formatted
	 * @return	The formatted task details
	 */
	private String formatTaskDetails(int index, Task task) {
		// Extracting task details
		String name = task.getName();
		String description = task.getDescription();
		if (description.equals("NIL")) {
			description = EMPTY_STRING;
		}
		String time = task.getDurationString();
		String status = task.getStatus().toString();

		// Formating
		String taskDetail = splitIntoLines(TABLE_FORMAT_NORMAL, name, description, time, status);
		int numDigits = String.valueOf(index).length();
		taskDetail = taskDetail.substring(numDigits);
		taskDetail = index + taskDetail;
		
		return taskDetail;
	}

	/**
	 * Split task details into a few lines if they exceed the field lengths
	 * @param format	The table format
	 * @param name	The name of task	
	 * @param description	The description of task
	 * @param time	The time of task
	 * @param status	The status of task
	 * @return	The formated string
	 */
	private String splitIntoLines(String format, String name, String description, String time, String status) {
		int lengthOfName = name.length();
		int lengthOfDescription = description.length();
		String formatedTask = EMPTY_STRING;

		int indexInName = 0;
		int indexInDescription = 0;
		int indexInTime = 0;
		while (indexInName < lengthOfName || indexInDescription < lengthOfDescription || time.length() > 0) {
			// Condition: at least one of the three fields have not been fully split
			String nameFrag = EMPTY_STRING;
			String descriptionFrag = EMPTY_STRING;
			String timeFrag = EMPTY_STRING;
			
			if (time.contains(EMPTY_LINE)) {
				// Only timed task occupies two lines in the time field
				indexInTime = time.indexOf('\n');
				timeFrag = time.substring(0, indexInTime);
				time = time.substring(indexInTime + 1);
			} else {
				timeFrag = time;
				time = EMPTY_STRING;
			}
			
			if (indexInName < lengthOfName) {
				// Condition: the last line in the task name field is still greater than LENGTH_NAME_FIELD
				nameFrag = name.substring(indexInName);
				if (nameFrag.length() > LENGTH_NAME_FIELD) {
					nameFrag = nameFrag.substring(0, LENGTH_NAME_FIELD - 1);
					indexInName += LENGTH_NAME_FIELD - 1;
					
					if (name.charAt(indexInName) != SPACE && name.charAt(indexInName + 1) != SPACE) {
						// Condition: a word splits from the middle
						nameFrag += "-";
					}
				} else {
					// nameFrag.length <= LENGTH_NAME_FIELD
					indexInName += nameFrag.length();
				}
			}
			
			if (indexInDescription < lengthOfDescription) {
				// Condition: the last line in the task description field is still greater than LENGTH_DESCRIPTION_FIELD
				descriptionFrag = description.substring(indexInDescription);
				if (descriptionFrag.length() > LENGTH_DESCRIPTION_FIELD) {
					descriptionFrag = descriptionFrag.substring(0, LENGTH_DESCRIPTION_FIELD - 1);
					indexInDescription += LENGTH_DESCRIPTION_FIELD - 1;
					
					if (description.charAt(indexInDescription) != SPACE && description.charAt(indexInDescription - 1) != SPACE) {
						// Condition: a word splits from the middle
						descriptionFrag += "-";
					}
				} else {
					// descriptionFrag <= LENGTH_DESCRIPTION_FIELD
					indexInDescription += descriptionFrag.length();
				}
			}
			
			nameFrag = nameFrag.trim();
			descriptionFrag = descriptionFrag.trim();
			
			formatedTask += String.format(format, EMPTY_STRING, nameFrag, descriptionFrag, timeFrag, status);
			status = EMPTY_STRING;
		}

		return formatedTask;
	}

	/**
	 * Write the available free slots in a proper format
	 * 
	 * @param tasks	The available free slots
	 */
	public void writeFreeSlots(List<Task> slots) {
		if (slots.size() == 0) {
			insert(_display.getStyle("normal"), NO_RESULT_MESSAGE);
			return;
		}
		int index = 1;
		DateHelper dh = new DateHelper();
		String heading = String.format(TABLE_FORMAT_FREE, EMPTY_STRING, "from", "to");
		insert(_display.getStyle("normal"), heading);
		for (Task slot : slots) {
			String slotDetails = String.format(TABLE_FORMAT_FREE, index++, dh.formatDateTimeToReadableString(slot.getStartingTime(), false), dh.formatDateTimeToReadableString(slot.getEndingTime(), false));
			insert(_display.getStyle("normal"), slotDetails);
		}
		insert(_display.getStyle("normal"), EMPTY_LINE);
	}
	
	/**
	 * Adjust table size according to current window size
	 */
	private void adjustTableSize() {
		Dimension currentDimension = this.getSize();
		double minWidth = DIMENSION_MINIMUM.getWidth();
		double currentWidth = currentDimension.getWidth();
		setFieldLength(currentWidth/minWidth);
	}
	
	/**
	 * Increase or decrease the window size by one INCREASE_STEP
	 * @param isIncrease	To increase or decrease
	 */
	public void setWindowSize(boolean isIncrease) {
		Dimension newDimension;
		double currentWidth = this.getSize().getWidth();
		double maxScreenWidth = Toolkit.getDefaultToolkit().getScreenSize().getWidth();
		
		if (isIncrease && currentWidth < maxScreenWidth) {
			newDimension = new Dimension((int)currentWidth + INCREASE_STEP, (int)this.getSize().getHeight());
		} else if (!isIncrease && currentWidth > DIMENSION_MINIMUM.getWidth()){
			newDimension = new Dimension((int)currentWidth - INCREASE_STEP, (int)this.getSize().getHeight());
		} else {
			return;
		}
		_console.setSize(newDimension);
		
	}
	
	/** 
	 * Set the field lengths of name and description proportionally
	 * @param increase	The ratio of current window size to minimum window size 
	 */
	private void setFieldLength(double increase) {
		if (increase > INCREASE_MINIMUM) {
			increase *= INCREASE_MULTIPLE;
		}
		LENGTH_NAME_FIELD = (int) (LENGTH_NAME_MINIMUM * increase);
		LENGTH_DESCRIPTION_FIELD = (int) (LENGTH_DESCRIPTION_MINIMUM * increase);
		setTableFormat();
	}
	
	/**
	 * Adjust the table format according to lengths of name and description field
	 */
	private void setTableFormat() {
		TABLE_FORMAT_NORMAL = "%1$-3s %2$-" + LENGTH_NAME_FIELD + "s | %3$-" + LENGTH_DESCRIPTION_FIELD + "s | %4$-25s | %5$-10s\n";
	}

	/**
	 * Display the welcome message, help message and today's task
	 */
	private void showWelcomePage() {
		insert(_display.getStyle("bold"), WELCOME_MESSAGE);
		DateHelper dh = new DateHelper();
		DateTime currentTime = dh.getCurrentDateTime();
		String today = String.format(TODAY_DATE, currentTime.getYear(), currentTime.getMonth(), currentTime.getDay());
		List<Task> todayTasks = _storage.searchTask(AttributeType.endingTime, true, today);
		List<Task> filter = _storage.searchTask(AttributeType.status, true, "done", "discard");
		todayTasks.removeAll(filter);
		writeMessage(String.format(TODAY_MESSAGE, today));
		writeTasks(todayTasks);
		insert(_display.getStyle("normal"), HELP_MESSAGE);
	}

	/**
	 * Entry point <br/>
	 * This method should be called after finishing instantiate the console
	 */
	public void start() {
		initialize();
		showWelcomePage();
		// run ();
	}
	
	/**
	 * Initialize other modules: Parser, History and Storage
	 */
	private void initialize() {
		_storage = Database.getInstance();
		_history = History.getInstance();
		_parser = new Parser(this);
	}

	/**
	 * Get the Storage module of this current session
	 * 
	 * @return Storage
	 */
	public Database getStorage() {
		return _storage;
	}

	/**
	 * Get the History module of this current session
	 * 
	 * @return History
	 */
	public History getHistory() {
		return _history;
	}

	//@author A0100965J
	/**
	 * "Clear" the console and show the welcome message
	 */
	public void clear() {
		_display.setText(EMPTY_STRING);
		showWelcomePage();
	}
	
	/**
	 * Clear the screen and import the "help" message from a text file
	 */
	public void showHelpPage() {
		_display.setText(EMPTY_STRING);
		File helpFile = new File(FILENAME);
		BufferedReader br = null;
		try {
			br = new BufferedReader(new InputStreamReader(new FileInputStream(helpFile)));
			String line;
			while ((line = br.readLine()) != null) {
				writeMessage(line + EMPTY_LINE);
			}
			_isAtHelpPage = true;
		} catch (FileNotFoundException e1) {
			writeMessage("Error displaying help message.");
		} catch (IOException e2) {
		} finally {
			if (br != null) {
				try {
					br.close();
				} catch (IOException e) {
				}
			}
		}	
	}
}
